﻿//************************************************************************************************
// Copyright © 2014 Steven M. Cohn. All Rights Reserved.
// 
//************************************************************************************************

namespace River.Folderize
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Security;
	using System.Security.AccessControl;
	using System.Security.Principal;
	using Microsoft.Win32;


	/// <summary>
	/// Manages known folders and custom folders displayed as immediate children of the
	/// "Computer" node in Windows Explorer.
	/// </summary>

	internal class FolderController
	{
		// The Big 6 - known folders
		public const string DesktopID = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}";
		public const string DownloadsID = "{374DE290-123F-4565-9164-39C4925E467B}";
		public const string DocumentsID = "{A8CDFF1C-4878-43be-B5FD-F8091C1C60D0}";
		public const string MusicID = "{1CF1260C-4DD0-4ebb-811F-33C572699FDE}";
		public const string PicturesID = "{3ADD1653-EB32-4cb0-BBD7-DFA0ABB5ACCA}";
		public const string VideosID = "{A0953C92-50DC-43bf-BE83-3742FED03C9C}";

		// Additional "knowns"
		public const string LibrariesID = "{1B3EA5DC-B587-4786-B4EF-BD1DC332AEAE}";
		public const string MyComputerID = "{20D04FE0-3AEA-1069-A2D8-08002B30309D}";
		public const string OneDriveID = "{8E74D236-7F35-4720-B138-1FED0B85EA75}";
		public const string UserLibrariesID = "{031E4825-7B94-4dc3-B131-E946B44C8DD5}";

		private const string NameSpacePath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\MyComputer\NameSpace";
		private const string PinnedName = "System.IsPinnedToNameSpaceTree";
		private const string ShellFileSystemID = "{0E5AAE11-A475-4C5B-AB00-C66DE400274E}";
		private const string TargetID = "{0AFACED1-E828-11D1-9187-B532F1E9575D}";

		private const string ShellDLL = @"%SystemRoot%\system32\shell32.dll";
		private const string DefaultIconRes = ShellDLL + ",3";
		private const string LibrariesDefaultIconRes = @"%SystemRoot%\system32\imageres.dll,202";


		/// <summary>
		/// 
		/// </summary>

		public static string EmptyIconRes
		{
			get { return DefaultIconRes; }
		}


		//========================================================================================
		// AddCustomFolder
		//========================================================================================

		/// <summary>
		/// Creates a new "Computer" level custom folder for the given path
		/// with a default icon.
		/// </summary>
		/// <param name="path">The absoluate path to assign to the custom folder.</param>
		/// <returns>A Folder instance.</returns>

		public void AddCustomFolder (CustomFolder folder)
		{
			CreateShellFolder(folder);
			AddNameSpaceFolderKey(folder.Guid, folder.Name);
		}


		#region AddCustomFolder helpers

		private void CreateShellFolder (Folder folder)
		{
			var parent = Registry.ClassesRoot.CreateSubKey(
				@"CLSID\" + folder.Guid, RegistryKeyPermissionCheck.ReadWriteSubTree);

			parent.SetValue(String.Empty, folder.Name);
			parent.SetValue("DescriptionID", 3, RegistryValueKind.DWord);
			parent.SetValue("InfoTip", String.Empty);
			parent.SetValue("System.IsPinnedToNameSpaceTree", 1, RegistryValueKind.DWord);

			string iconRes = folder.ImageRes ?? GetDesktopIconResource(folder.Path);

			using (var key = parent.CreateSubKey("DefaultIcon", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				if (iconRes != null)
				{
					key.SetValue(String.Empty, iconRes);
					folder.ImageRes = iconRes;
				}
			}

			using (var key = parent.CreateSubKey("InProcServer32", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue(String.Empty, @"%SystemRoot%\system32\shdocvw.dll", RegistryValueKind.ExpandString);
				key.SetValue("ThreadingModel", "Apartment");
			}

			using (var key = parent.CreateSubKey("Instance", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue("CLSID", TargetID);
				using (var sub = key.CreateSubKey("InitPropertyBag", RegistryKeyPermissionCheck.ReadWriteSubTree))
				{
					sub.SetValue("Attributes", 17, RegistryValueKind.DWord);
					sub.SetValue("Target", folder.Path);
					sub.Close();
				}
			}

			using (var key = parent.CreateSubKey("ShellFolder", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue("Attributes", -125828771, RegistryValueKind.DWord);
				key.SetValue("FolderValueFlags", 9, RegistryValueKind.DWord);
				key.SetValue("SortOrderIndex", 0, RegistryValueKind.DWord);
				key.SetValue("WantsFORPARSING", String.Empty, RegistryValueKind.String);
			}

			parent.Dispose();
		}

		private void AddNameSpaceFolderKey (string guid, string name)
		{
			using (var parent = Registry.LocalMachine.OpenSubKey(NameSpacePath, true))
			{
				var key = parent.OpenSubKey(guid);
				if (key == null)
				{
					key = parent.CreateSubKey(guid, RegistryKeyPermissionCheck.ReadWriteSubTree);

					if (name != null)
					{
						key.SetValue(String.Empty, name);
					}
				}
				else
				{
					key.Dispose();
				}
			}
		}


		private string GetDesktopIconResource (string folderPath)
		{
			string iconRes = null;
			string topFile = Path.Combine(folderPath, "Desktop.ini");
			if (File.Exists(topFile))
			{
				try
				{
					using (var reader = File.OpenText(topFile))
					{
						while (!reader.EndOfStream)
						{
							string line = reader.ReadLine();
							if (line.StartsWith("IconResource="))
							{
								iconRes = line.Substring(13);
								break;
							}
						}

						reader.Close();
					}
				}
				catch
				{
				}
			}

			return iconRes ?? DefaultIconRes;
		}

		#endregion AddCustomFolder helpers


		//========================================================================================
		// AddLibrariesFolder
		//========================================================================================

		/// <summary>
		/// Create a "Computer" level Libaries folder; this is reference to the known
		/// system folder named "Libraries".
		/// </summary>
		/// <returns>A Folder instance.</returns>

		public Folder AddLibrariesFolder ()
		{
			var folder = new CustomFolder((Guid.NewGuid()).ToString("B"));
			folder.Name = "Libraries";
			folder.ImageRes = LibrariesDefaultIconRes;

			var parent = Registry.ClassesRoot.CreateSubKey(
				@"CLSID\" + folder.Guid, RegistryKeyPermissionCheck.ReadWriteSubTree);

			parent.SetValue(String.Empty, folder.Name);
			parent.SetValue("DescriptionID", 3, RegistryValueKind.DWord);
			parent.SetValue("InfoTip", String.Empty);
			parent.SetValue(PinnedName, 1, RegistryValueKind.DWord);

			using (var ikey = parent.CreateSubKey("DefaultIcon", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				if (!String.IsNullOrEmpty(folder.ImageRes))
				{
					ikey.SetValue(String.Empty, folder.ImageRes, RegistryValueKind.ExpandString);
					folder.Image = ImageHelper.LoadIcon(folder.ImageRes, ImageHelper.ImageType.Folder);
				}
			}

			using (var key = parent.CreateSubKey("InProcServer32", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue(String.Empty, ShellDLL, RegistryValueKind.ExpandString);
				key.SetValue("ThreadingModel", "Both");
			}

			using (var key = parent.CreateSubKey("Instance", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue("CLSID", ShellFileSystemID);
				using (var sub = key.CreateSubKey("InitPropertyBag", RegistryKeyPermissionCheck.ReadWriteSubTree))
				{
					sub.SetValue("Attributes", "0x00000011");
					sub.SetValue("RecursiveSearch", 1, RegistryValueKind.DWord);
					sub.SetValue("TargetKnownFolder", LibrariesID);
					folder.Path = LibrariesID;
				}
			}

			using (var key = parent.CreateSubKey("ShellFolder", RegistryKeyPermissionCheck.ReadWriteSubTree))
			{
				key.SetValue("Attributes", 0x20000004, RegistryValueKind.DWord);
				key.SetValue("FolderValueFlags", 0x00000438, RegistryValueKind.DWord);
			}

			parent.Dispose();

			AddNameSpaceFolderKey(folder.Guid, folder.Name);

			return folder;
		}


		//========================================================================================
		// GetFolders
		//========================================================================================

		/// <summary>
		/// Retrieve a list of currently defined folders under the NameSpace key.
		/// This includes both known system folder and user-defined custom folders.
		/// </summary>
		/// <returns>An unordered List of Folder isntances.</returns>

		public List<Folder> GetFolders ()
		{
			var folders = new List<Folder>();

			using (var key = Registry.LocalMachine.OpenSubKey(NameSpacePath))
			{
				var names = key.GetSubKeyNames();
				if ((names != null) && (names.Length > 0))
				{
					foreach (string name in names)
					{
						if (name.Equals(DesktopID) ||
							name.Equals(DocumentsID) ||
							name.Equals(DownloadsID) ||
							name.Equals(MusicID) ||
							name.Equals(PicturesID) ||
							name.Equals(VideosID))
						{
							var folder = new SystemFolder(name);
							GetFolderDetails(folder);
							folders.Add(folder);
						}
						// skip "DelegateFolders"... and for some reason, GetSubKeyNames returns
						// all the GUID names without curly braces, doubling the overall count
						// of names.  So here, we only select the fully-formatted GUID names.
						else if (name[0] == '{')
						{
							var folder = new CustomFolder(name);
							GetFolderDetails(folder);
							folders.Add(folder);
						}
					}
				}
			}

			return folders;
		}


		#region GetFolders helpers

		private void GetFolderDetails (Folder folder)
		{
			// lookup class name
			try
			{
				var root = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid);
				if (root == null)
				{
					// special case: key doesn't exist!
					root.Dispose();
					folder.Guid = null;
					return;
				}

				var value = root.GetValue(String.Empty);
				if (value != null)
				{
					folder.Name = value.ToString();
				}

				root.Dispose();
			}
			catch
			{
				folder.Guid = null;
				return;
			}

			if (folder.IsSystemFolder && String.IsNullOrEmpty(folder.Name))
			{
				switch (folder.Guid)
				{
					case DesktopID: folder.Name = "Desktop"; break;
					case DownloadsID: folder.Name = "Downloads"; break;
					case DocumentsID: folder.Name = "Documents"; break;
					case MusicID: folder.Name = "Music"; break;
					case PicturesID: folder.Name = "Pictures"; break;
					case VideosID: folder.Name = "Videos"; break;

					case OneDriveID: folder.Name = "OneDrive"; break;
					case UserLibrariesID: folder.Name = "User Libraries"; break;
			}
			}

			// lookup path
			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid + @"\Instance\InitPropertyBag"))
			{
				if (key != null)
				{
					folder.Path = (string)key.GetValue("Target", String.Empty);
					if (folder.Path.Equals(String.Empty))
					{
						folder.Path = (string)key.GetValue("TargetKnownFolder", String.Empty);
					}
				}
			}

			// lookup class icon
			try
			{
				using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid + @"\DefaultIcon"))
				{
					folder.ImageRes = (string)key.GetValue(String.Empty, DefaultIconRes);
					folder.Image = ImageHelper.LoadIcon(folder.ImageRes, ImageHelper.ImageType.Folder);
				}
			}
			catch
			{
			}

			// lookup "show in nav pane" pinned flat
			if (folder.IsSystemFolder)
			{
				folder.IsPinned = true;
			}
			else
			{
				using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid))
				{
					folder.IsPinned = ((int)key.GetValue(PinnedName) > 0);
				}
			}

			// special folder sort order index
			if (folder.Guid.Equals(OneDriveID) || folder.Guid.Equals(UserLibrariesID))
			{
				using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid))
				{
					((SpecialFolder)folder).SortOrderIndex = (int)key.GetValue("SortOrderindex");
				}
			}
		}

		#endregion // GetFolders helpers


		//========================================================================================
		// Minor folder management
		//========================================================================================

		/// <summary>
		/// Removes the NameSpace key associating with the specified system folder. 
		/// These keys do not have values or subkeys.
		/// </summary>
		/// <param name="guid">The GUID of the system folder to remove.</param>

		public void ExcludeSystemFolder (string guid)
		{
			using (var parent = Registry.LocalMachine.OpenSubKey(NameSpacePath, true))
			{
				parent.DeleteSubKey(guid, false);
			}
		}


		/// <summary>
		/// Adds the NameSpace key for the specified system folder.  These keys only need to
		/// be created without any values or subkeys.
		/// </summary>
		/// <param name="guid">The GUID of the desired system folder.</param>

		public void IncludeSystemFolder (string guid)
		{
			AddNameSpaceFolderKey(guid, null);
		}


		/// <summary>
		/// Determines if the specified custom folder is included in the left-hand
		/// navigation panel of Windows Explorer.
		/// </summary>
		/// <param name="guid">The GUID of the custom folder.</param>
		/// <returns>A Boolean value indicating if the folder is visible in the Windows
		/// Explorer navigation panel.</returns>

		public bool IsPinned (string guid)
		{
			bool pinned = false;
			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + guid))
			{
				int result = (int)key.GetValue(PinnedName, 0);
				pinned = result > 0;
			}

			return pinned;
		}


		/// <summary>
		/// Gets the details of the specified system folder even if that folder is not
		/// currently accessible through Windows Explorer (the GUID is not included in the
		/// NameSpace Registry node.)
		/// </summary>
		/// <param name="guid">The GUID of the desired system folder.</param>
		/// <returns>A new Folder instance.</returns>

		public Folder Lookup<T> (string guid) where T : Folder, new()
		{
			T folder = new T();
			folder.Guid = guid;
			GetFolderDetails(folder);

			// the only reason we called LookupSystemFolder is because the folder is missing!
			folder.IsPinned = false;

			return folder;
		}


		/// <summary>
		/// Includes the specified custom folder in the left-hand navigation panel of
		/// Windows Explorer.
		/// </summary>
		/// <param name="guid">The GUID of the custom folder.</param>
		/// <remarks>
		/// <para>
		/// To affect just the current user, set System.IsPinnedToNameSpaceTree under
		/// HKEY_CURRENT_USER\Software\Classes\CLSID\{guid} as DWORD 1 (to show).
		/// </para>
		/// <para>
		/// To affect all users, modify the Attributes value under
		/// HKEY_CLASSES_ROOT\CLSID\{guid}\ShellFolder removing the NONENUMERATED flag
		/// which is bit offset 20 (0x00100000).
		/// </para>
		/// </remarks>

		public void Pin (string guid)
		{
			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + guid, true))
			{
				key.SetValue(PinnedName, 1, RegistryValueKind.DWord);
			}

			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + guid + @"\ShellFolder", true))
			{
				try
				{
					int attributes = (int)key.GetValue("Attributes");
					if ((attributes & (int)Native.ShellFolderAttributes.Nonenumerated) > 0)
					{
						attributes ^= (int)Native.ShellFolderAttributes.Nonenumerated;
						key.SetValue("Attributes", attributes, RegistryValueKind.DWord);
					}
				}
				catch { }
			}
		}


		/// <summary>
		/// Deletes the specified custom folder, removing all remnants from the Registry
		/// including both the NameSpace entry and the detailed CLSID subtree.
		/// </summary>
		/// <param name="guid">The ID of the custom folder to delete.</param>

		public void RemoveCustomFolder (string guid)
		{
			using (var key = Registry.ClassesRoot.OpenSubKey("CLSID", true))
			{
				key.DeleteSubKeyTree(guid, false);
			}

			using (var key = Registry.LocalMachine.OpenSubKey(NameSpacePath, true))
			{
				key.DeleteSubKeyTree(guid, false);
			}
		}


		/// <summary>
		/// 
		/// </summary>
		/// <param name="folder"></param>

		public void SaveProperties (CustomFolder folder)
		{
			using (var root = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + folder.Guid, true))
			{
				string name = (string)root.GetValue(String.Empty, String.Empty);
				if (!name.Equals(folder.Name))
				{
					root.SetValue(String.Empty, folder.Name);
				}

				using (var key = root.OpenSubKey("DefaultIcon", true))
				{
					if (key != null)
					{
						key.SetValue(String.Empty, folder.ImageRes);
					}
				}

				if (folder.Path[0] != '{')
				{
					using (var key = root.OpenSubKey(@"Instance\InitPropertyBag", true))
					{
						key.SetValue("Target", folder.Path);
					}
				}
			}
		}


		/// <summary>
		/// Change the sort order of the specified folder.  These are relative indexes that
		/// ideally should be different than other items at the same level.
		/// </summary>
		/// <param name="guid">The ID of the folder.</param>
		/// <param name="index">The relative sort order index to set.</param>
		/// <example>
		/// <para>UserLibs top index=0x42, bot index=0x54</para>
		/// <para>OneDrive top index=0x42, bot index=0x50</para>
		/// </example>

		public int SetSortOrderIndex (string guid, bool top)
		{
			int index = -1;
			if (guid.Equals(UserLibrariesID))
			{
				index = top ? 0x42 : 0x54;
			}
			else if (guid.Equals(OneDriveID))
			{
				index = top ? 0x42 : 0x50;
			}

			if (index >= 0)
			{
				try
				{
					using (var key = OpenWritableKey(
						Registry.ClassesRoot, @"CLSID\" + guid, RegistryRights.SetValue))
					{
						key.SetValue("SortOrderIndex", index, RegistryValueKind.DWord);
					}
				}
				catch
				{
					// OneDrive key typically doesn't even have Admin permission
					index = -1;
				}
			}

			return index;
		}


		#region OpenWritableKey

		// Some keys, such as the OneDrive key, seem to be created without administrative
		// rights so we need a special function to open the key and elevate if necessary.

		private static RegistryKey OpenWritableKey (
			RegistryKey parentKey, string path, RegistryRights desiredRights)
		{
			RegistryKey key = null;

			try
			{
				key = parentKey.OpenSubKey(
					path, RegistryKeyPermissionCheck.ReadWriteSubTree, desiredRights);
			}
			catch (SecurityException)
			{
				RegistrySecurity control = null;

				using (key = parentKey.OpenSubKey(
					path, RegistryKeyPermissionCheck.Default, RegistryRights.ReadPermissions))
				{
					var admins = @"BUILTIN\Administrators";

					var rule = new RegistryAccessRule(
						@admins,
						RegistryRights.TakeOwnership | RegistryRights.ChangePermissions,
						AccessControlType.Allow);

					control = key.GetAccessControl(AccessControlSections.All);
					control.AddAccessRule(rule);
					control.SetOwner(new NTAccount(admins));
				}

				using (key = parentKey.OpenSubKey(
					path, RegistryKeyPermissionCheck.ReadWriteSubTree,
					RegistryRights.ReadKey | RegistryRights.TakeOwnership))
				{
					key.SetAccessControl(control);
				}

				try
				{
					key = parentKey.OpenSubKey(
						path, RegistryKeyPermissionCheck.ReadWriteSubTree, desiredRights);
				}
				catch
				{
					key = null;
				}
			}
			catch (Exception)
			{
				if (key != null)
				{
					key.Dispose();
					key = null;
				}
			}

			return key;
		}

		#endregion OpenWritableKey


		/// <summary>
		/// Excludes the specified custom folder from the left-hand navigation panel of
		/// Windows Explorer without actually deleting the folder.
		/// </summary>
		/// <param name="guid">The GUID of the custom folder.</param>
		/// <remarks>
		/// <para>
		/// To affect just the current user, set System.IsPinnedToNameSpaceTree under
		/// HKEY_CURRENT_USER\Software\Classes\CLSID\{guid} as DWORD 0 (to hide).
		/// </para>
		/// <para>
		/// To affect all users, modify the Attributes value under
		/// HKEY_CLASSES_ROOT\CLSID\{guid}\ShellFolder adding the NONENUMERATED flag
		/// which is bit offset 20 (0x00100000).
		/// </para>
		/// </remarks>

		public void Unpin (string guid)
		{
			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + guid, true))
			{
				key.SetValue(PinnedName, 0, RegistryValueKind.DWord);
			}

			using (var key = Registry.ClassesRoot.OpenSubKey(@"CLSID\" + guid + @"\ShellFolder", true))
			{
				try
				{
					int attributes = (int)key.GetValue("Attributes");
					if ((attributes & (int)Native.ShellFolderAttributes.Nonenumerated) == 0)
					{
						attributes |= (int)Native.ShellFolderAttributes.Nonenumerated;
						key.SetValue("Attributes", attributes, RegistryValueKind.DWord);
					}
				}
				catch { }
			}
		}
	}
}
